﻿/*
 * RichTextDocument
 * 
 * Handle text save / open actions
 * 
 * Saved file format brief introduction
 * 
 *      Wrapper format : Zip(Package)
 *      
 *      Name                    Description                                                                   Uri
 *      DocumentOverview.xml    Specify all necessary information                                             Manifest\DocumentOverview.xml
 *      Resources.xml           List all embeded resources                                                    Manifest\Resources.xml
 *      xxxx.xaml           Document xaml file                                                                Document\xxxx.xaml
 *      Uri: ReferencedResources     Contains all referenced resources                                        Resources\xxxx
 *      Each referenced resources is saved as a single file
 *      
 * Open progress
 *  
 *      Open Package -> Load DocumentOverview -> Load Document -> Load Resources Manifest -> Load And Assign Resources
 * 
 * Save progress
 * 
 *      Create Package -> Generate DocumentOverview -> Scan RichTextDocument, Assign Resource Key, Generate Resources Manifest -> WriteResources  ->  Write Document -> Write Document overview -> Write Resources Manifest 
 *      
 */

using System;
using System.IO;
using System.IO.Packaging;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Documents;
using System.Windows.Media;
using System.Windows.Markup;
using System.Xml.Serialization;
using System.Net.Mime;
using Snowdreamist.ComponentModel;

namespace Snowdreamist.Windows.Document
{
    public class RichTextDocument : FlowDocument
    {
        #region static 
        /// <summary>
        /// 
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static bool GetIsReadonly(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsReadonlyProperty);
        }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="value"></param>
        public static void SetIsReadonly(DependencyObject obj, bool value)
        {
            obj.SetValue(IsReadonlyProperty, value);
        }
        /// <summary>
        /// Is this document readonly
        /// </summary>
        public static readonly DependencyProperty IsReadonlyProperty =
            DependencyProperty.RegisterAttached("IsReadonly", typeof(bool), typeof(RichTextDocument), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits));
        /// <summary>
        /// Get resource key of document element
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static Guid GetResourceKey(DependencyObject obj)
        {
            return (Guid)obj.GetValue(ResourceKeyProperty);
        }
        /// <summary>
        /// Set resource key of document element
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="value"></param>
        public static void SetResourceKey(DependencyObject obj, Guid value)
        {
            obj.SetValue(ResourceKeyProperty, value);
        }
        /// <summary>
        /// The resource key which is used to make relation between resource and document element
        /// </summary>
        public static readonly DependencyProperty ResourceKeyProperty =
            DependencyProperty.RegisterAttached("ResourceKey", typeof(Guid), typeof(RichTextDocument), new PropertyMetadata(Guid.Empty));
        /// <summary>
        /// Document title
        /// </summary>
        public string Title
        {
            get { return (string)GetValue(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }
        /// <summary>
        /// The title of document
        /// </summary>
        public static readonly DependencyProperty TitleProperty =
            DependencyProperty.Register("Title", typeof(string), typeof(RichTextDocument), new UIPropertyMetadata(null));
        /// <summary>
        /// Get is in design mode
        /// </summary>
        /// <param name="obj"></param>
        /// <returns></returns>
        public static bool GetIsDesignMode(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsDesignModeProperty);
        }
        /// <summary>
        /// Set is in design mode
        /// </summary>
        /// <param name="obj"></param>
        /// <param name="value"></param>
        public static void SetIsDesignMode(DependencyObject obj, bool value)
        {
            obj.SetValue(IsDesignModeProperty, value);
        }
        /// <summary>
        /// Is this document in design mode, inherit
        /// </summary>
        public static readonly DependencyProperty IsDesignModeProperty =
            DependencyProperty.RegisterAttached("IsDesignMode", typeof(bool), typeof(RichTextDocument), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.Inherits));
        /// <summary>
        /// Read richtext from stream
        /// </summary>
        /// <param name="stream"></param>
        /// <returns></returns>
        public static IEnumerable<RichTextDocument> Read(Stream stream, out DocumentOverview overview, bool isFailMissingResource = false)
        {
            if (stream == null)
                throw new ArgumentNullException("stream");
            // load package
            using (var package = Package.Open(stream, FileMode.Open, FileAccess.Read))
            {
                overview = GetOverview(package);
                if (overview == null)
                    overview = new DocumentOverview();

                var documents = GetDocuments(package, overview);
                if (documents == null || documents.Count() == 0)
                    throw new ArgumentException("Cannot load document content");

                DocumentResources resources = GetResources(package);
                if (resources != null)
                {
                    // load resources
                    foreach(var document in documents)
                        TryAssignResources(document, package, resources, isFailMissingResource);
                }

                return documents;
            }
        }
        /// <summary>
        /// Save rich text document
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="documents"></param>
        /// <param name="isPersistReferencedResource"></param>
        public static void Save(Stream stream, IEnumerable<RichTextDocument> documents, bool isPersistReferencedResource = true)
        {
            Save(stream, documents, new DocumentOverview(), isPersistReferencedResource);
        }
        /// <summary>
        /// Save rich text document
        /// </summary>
        /// <param name="stream"></param>
        /// <param name="document"></param>
        /// <param name="overview"></param>
        /// <param name="isPersistReferencedResource"></param>
        public static void Save(Stream stream, IEnumerable<RichTextDocument> documents, DocumentOverview overview, bool isPersistReferencedResource = true)
        {
            using (Package package = Package.Open(stream, FileMode.Create, FileAccess.Write))
            using (RichTextDocumentPersistingContext context = new RichTextDocumentPersistingContext(package))
            {
                // over all overview
                if (overview == null)
                    overview = new DocumentOverview();
                // reset
                overview.Filename = null;
                overview.Children = new List<DocumentOverview>();
                // save documents
                foreach (var document in documents)
                {
                    // generate overview
                    var documentOverview = document.GenerateOverview();
                    // set filename
                    documentOverview.Filename = "Document-" + ThreadSafeRandom.Next().ToString();
                    overview.Children.Add(documentOverview);
                    // get resources
                    if (isPersistReferencedResource)
                    {
                        // write resources
                        AchieveResources(document, context);
                    }
                    // write document content
                    var documentPackagePart = package.CreatePart(new Uri(string.Format(Utility.DocumentUriFormat, documentOverview.Filename), UriKind.Relative), MediaTypeNames.Text.Xml);
                    using (var partStream = documentPackagePart.GetStream())
                    {
                        XamlWriter.Save(document, partStream);
                    }
                }
                // write document overview
                var overviewPackagePart = package.CreatePart(Utility.DocumentOverviewUri, MediaTypeNames.Text.Xml);
                using (var partStream = overviewPackagePart.GetStream())
                {
                    _documentOverviewXmlSerializer.Serialize(partStream, overview);
                }
                // write resource manifest
                var resourcePackagePart = package.CreatePart(Utility.ResourcesUri, MediaTypeNames.Text.Xml);
                using (var partStream = resourcePackagePart.GetStream())
                {
                    _documentResourcesXmlSerializer.Serialize(partStream, context.Resources);
                }
            }
        }



        #endregion

        public void Save(Stream stream, bool isPersistReferencedResource = true)
        {
            if (stream == null)
                throw new ArgumentNullException("stream");

            Save(stream, new RichTextDocument[] { this }, isPersistReferencedResource);
        }

        #region properties

        #endregion

        #region protected

        protected virtual DocumentOverview GenerateOverview()
        {
            return new DocumentOverview()
            {
                Title = this.Title
            };
        }

        protected virtual void ResolveOverview(DocumentOverview overview)
        {
            if (overview != null)
                this.Title = overview.Title;
        }

        #endregion

        #region private

        #region static

        private static XmlSerializer _documentOverviewXmlSerializer = new XmlSerializer(typeof(DocumentOverview), Utility.Namespace);
        
        private static XmlSerializer _documentResourcesXmlSerializer = new XmlSerializer(typeof(DocumentResource), Utility.Namespace);

        private static DocumentOverview GetOverview(Package package)
        {
            // load overview
            try
            {
                var overviewPackagePart = package.GetPart(Utility.DocumentOverviewUri);
                using (var xmlStream = overviewPackagePart.GetStream())
                {
                    return _documentOverviewXmlSerializer.Deserialize(xmlStream) as DocumentOverview;
                }
            }
            catch (InvalidOperationException)
            {
                // resources manifest doesn't exist, ignore
            }
            catch
            {
                throw;
            }

            return null;
        }

        private static DocumentResources GetResources(Package package)
        {
            var resourcesPackagePart = package.GetPart(Utility.ResourcesUri);
            using(var xmlStream = resourcesPackagePart.GetStream())
            {
                return _documentResourcesXmlSerializer.Deserialize(xmlStream) as DocumentResources;
            }
        }
        
        private static IEnumerable<RichTextDocument> GetDocuments(Package package, DocumentOverview overview)
        {
            if (string.IsNullOrWhiteSpace(overview.Filename))
            {
                if (overview.Children == null || overview.Children.Count == 0)
                    return null;

                IEnumerable<RichTextDocument> documents = Enumerable.Empty<RichTextDocument>();

                foreach (var child in overview.Children)
                    documents = documents.Concat(GetDocuments(package, child));

                return documents;
            }
            else
            {
                var documentPackagePart = package.GetPart(new Uri(string.Format(Utility.DocumentUriFormat, overview.Filename), UriKind.Relative));
                using (var xamlStream = documentPackagePart.GetStream())
                {
                    RichTextDocument document = XamlReader.Load(xamlStream) as RichTextDocument;
                    // resolve overview
                    document.ResolveOverview(overview);
                    return new RichTextDocument[] { document };
                }
            }
        }

        private static void TryAssignResources(DependencyObject dpObj, Package package, DocumentResources resources, bool isThrownWhenFailed)
        {
            if (dpObj is IHoldReferencedResource)
            {
                IHoldReferencedResource holdRefResObj = dpObj as IHoldReferencedResource;
                // assign
                var key = GetResourceKey(dpObj);

                if (key != Guid.Empty)
                {
                    try
                    {
                        DocumentResource resource = null;
                        if (resources.Collections.TryGetValue(key, out resource) || resource == null)
                        {
                            foreach (var metaResource in resource.Values)
                            {
                                Uri resourceUri = new Uri(string.Format(Utility.ResourceUriFormat, metaResource.Filename), UriKind.Relative);
                                var resourcePackagePart = package.GetPart(resourceUri);
                                using (var stream = resourcePackagePart.GetStream())
                                {
                                    holdRefResObj.LoadResource(metaResource.Key, metaResource.Type, stream);
                                }
                            }
                        }
                        else
                        {
                            if (isThrownWhenFailed)
                                throw new InvalidOperationException("Cannot find resources manifest");
                        }
                    }
                    catch
                    {
                        if (isThrownWhenFailed)
                            throw;
                    }
                }
            }

            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dpObj); ++i)
                TryAssignResources(VisualTreeHelper.GetChild(dpObj, i), package, resources, isThrownWhenFailed);
        }

        private static void AchieveResources(DependencyObject dpObj, RichTextDocumentPersistingContext context)
        {
            if (dpObj is IHoldReferencedResource)
            {
                IHoldReferencedResource holdRefResObj = dpObj as IHoldReferencedResource;

                holdRefResObj.SaveResources(context);
            }

            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dpObj); ++i)
                AchieveResources(dpObj, context);
        }

        #endregion

        #endregion
    }
}
